iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0
自我挑戰組

基於自然語言處理的新聞意見提取應用開發筆記系列 第 13

[Day-13] 以 Spacy 的 DependencyMatcher 找出意見持有者、動詞、句子範圍

  • 分享至 

  • xImage
  •  

Day-13 內容

  • 使用 DependencyMatcher
    • match pattern 設計
    • 執行 match
    • 將結果存成 span 並視覺化
  • 整篇範例新聞的視覺化結果

注意:今天的文章與昨天的有連貫性喔,所以建議先去看 [Day-12] 解析 Spacy 的 Dependency Parsing

今天要繼續設計從 Spacy 的 Dependency Parsing 結果中,找出意見持有者、意見動詞、意見句子範圍的方法。也就是做出下圖中的效果。
https://ithelp.ithome.com.tw/upload/images/20220928/20152690GHTHGMn5CX.png

昨天 [Day-12] 解析 Spacy 的 Dependency Parsing 的範例中,使用到以下程式碼片段來篩選出 Dependency Parsing 中的有用資訊:

if token.tag_ == 'VE':
            subj = [w for w in chain(token.lefts, token.rights) if w.ent_type_ == "PERSON" or w.ent_type_ == "ORG"]
            for s in subj:
                print(f"{token}({token.tag_, token.ent_type_}) ---{s.dep_}---> {s}({s.tag_, s.ent_type_}) => {[t for t in s.subtree]}")

上方程式碼可以從以下範例文字的 Dependency Parsing 結果中,獲取上面程式碼規則所定義的目標資訊:

  • 範例輸入文字:
媒體關注數位部部長唐鳳對數位中介服務法看法,唐鳳表示,監理業務不屬於數位部範圍,面對大型跨境數位平台,最重要的是要確保現實世界中覺得合理的價值,平台上也應該符合相關的社會價值,遵守常規。
  • 獲得的目標資訊:
表示(('VE', '')) ---nsubj---> 唐(('Nb', 'PERSON')) => [唐, 鳳]

然而這樣單單使用條件(e.g. if)與迴圈(e.g. for)所組成的分析碼規,並不是最好的方式,之後當規則需要越來越複雜時,這樣的寫法容易導致更動困難與不易維護。所以今天就要來介紹更好的替代方案,那就是使用 Spacy 的 DependencyMatcher,這個做法後續還可以將找到的意見持有者、意見動詞、意見句子範圍做視覺化的標注呈現,超棒的!

等等會先使用到昨天的程式碼片段:

import stanza
import spacy_stanza
from ckip_transformers.nlp import CkipPosTagger, CkipNerChunker, CkipWordSegmenter
import spacy
from spacy import displacy
from itertools import chain

stanza.download("zh-hant")
nlp = spacy_stanza.load_pipeline("xx", lang='zh-hant')

def add_ner(doc):
    ner_driver = CkipNerChunker(model="bert-base")
    ner = ner_driver([str(doc)], show_progress=False)
    ner_spans = []
    for entity in ner[0]:
        ner_spans.append(doc.char_span(entity.idx[0], entity.idx[1], label=entity.ner))
    orig_ents = list(doc.ents)
    doc.ents = orig_ents + ner_spans

def add_ckip_tag(doc):
    pos_driver = CkipPosTagger(model="bert-base")
    words = [[str(token) for token in doc]]
    pos = pos_driver(words, show_progress=False)
    for token, ckip_pos in zip(doc, pos[0]):
        token.tag_ = ckip_pos
        
content = ['媒體關注數位部部長唐鳳對數位中介服務法看法,唐鳳表示,監理業務不屬於數位部範圍,面對大型跨境數位平台,最重要的是要確保現實世界中覺得合理的價值,平台上也應該符合相關的社會價值,遵守常規。',
 '唐鳳今天中午接受震傳媒網路節目「新聞不芹菜」的視訊採訪,主持人黃光芹詢問唐鳳對於數位中介服務法(中介法)看法,是否反對立法。',
 '唐鳳表示,中介法主要是監理非法言論部份,在數位部揭牌當天就提到,數位部的工作是扮演採油門角色,監理部份並不是數位部業務。',
 '唐鳳直言,NCC日前提出數位中介服務法草案,還在NCC對外界徵詢草案的階段,目前NCC也提到會整理相關民間意見、歸零思考,把草案退回到工作小組。草案先前也還沒有提到政院討論,沒有要表態的問題。',
 '唐鳳指出,面對跨境大型數位平台時,最重要價值是要確保現實社會覺得合理的價值,在大型數位平台上也應該符合相關的社會價值。',
 '唐鳳舉例,監察院會公布競選期間政治獻金花費,選罷法也規範境外者不能給予候選人政治獻金,必須要符合透明度以及不能收國外的錢。前幾年有人透過數位平台下廣告、幫特定候選人打廣告,等於繞過政治獻金法的規範。',
 '唐鳳指出,後來也有跟平台溝通,不管在美國是什麼樣的規範,在台灣就是要揭露、不能收國外的錢,不能到平台上就不遵守相關常規。平台後來在2019年做修正,舉這個例子是要說明,還是要從社會怎麼樣合適出發,這些跨境科技平台要配合社會價值,而不是擾亂社會價值、反過來要求配合平台。']

使用 DependencyMatcher

match pattern 設計

DependencyMatcher 的主要功能是讓使用者能透過自定義的 pattern 來在 Dependency Parsing 的結果中,找尋與 pattern 中規則對應的 Token。因此使用 DependencyMatcher 的第一步就是要根據需要的配對規則設計pattern

以下是我設計的規則範例:

  1. 先找到 TAG 是 VE 的 Token(此處為昨天提到的 CKIP POS tag),此為意見動詞的 Token
  2. 確認上一步找到的 VE Token 是否有對外關係標註為 nsubj 的箭頭,找到其指向的 Token,此為意見持有者樹狀結構的 root(可能找到多個,後續會進一步處理)。
  3. 確認上一步找到的 VE Token 是否有對外關係標註為 ccomp 或 parataxis 的箭頭,此為意見句樹狀結構的 root(可能找到多個,後續會進一步處理)。

pattern 的 Python 程式碼如下:

pattern = [
  {
    "RIGHT_ID": "VE",
    "RIGHT_ATTRS": {"TAG": "VE"}
  },
  {
    "LEFT_ID": "VE",
    "REL_OP": ">",
    "RIGHT_ID": "who_root",
    "RIGHT_ATTRS": {"DEP": "nsubj"}
  },
  {
    "LEFT_ID": "VE",
    "REL_OP": ">",
    "RIGHT_ID": "idea_root",
    "RIGHT_ATTRS": {"DEP": {"IN": ["ccomp", "parataxis"]}}
  }
]

上面程式碼 pattern 中的三個 dict 照順序分別對應到我設計的三個歸則。

pattern 的設計說明在 Dependency Matcher


執行 match

先以 content 的第一段文字來演釋 match 使用:

doc = content[0]
add_ner(doc)
add_ckip_tag(doc)

content[0] 的文字內容是:

媒體關注數位部部長唐鳳對數位中介服務法看法,唐鳳表示,監理業務不屬於數位部範圍,面對大型跨境數位平台,最重要的是要確保現實世界中覺得合理的價值,平台上也應該符合相關的社會價值,遵守常規。

content[0] 的 Dependency Parsing 結果是:
https://ithelp.ithome.com.tw/upload/images/20220928/20152690gqVlHNudbV.png

完整的樹狀結構請見此連結 中的第一個樹狀結構。

我設計了下方的程式碼來執行 match:

from spacy.matcher import DependencyMatcher

matcher = DependencyMatcher(nlp.vocab, validate=True)
matcher.add("Rule0", [pattern])

matches = matcher(doc)
matches_sorted = sorted(matches, key=lambda x: abs(x[1][0] - x[1][1]))
if len(matches_sorted) > 1:
  matches_sorted = [match for match in matches_sorted if (match[1][0] == matches_sorted[0][1][0] and match[1][1] == matches_sorted[0][1][1])]
print(matches_sorted)

此段落 import DependencyMatcher 並建立 instance,再將前面定義的 pattern 加到 instance。

from spacy.matcher import DependencyMatcher

matcher = DependencyMatcher(nlp.vocab, validate=True)
matcher.add("Rule0", [pattern])

獲得 match 的初步結果:

matches = matcher(doc)

此時結果為 [(2992876933063962013, [16, 14, 36])],當中 [16, 14, 36] 依序代表了 pattern 中三個步驟所配對到的 Token index。也就是說

  • 第 16 個 Token 是「表示(VE)」。
  • 第 14 個 Token 是「唐(who_root)」,由「表示(VE)」透過 nsubj 的箭頭指到。
  • 第 14 個 Token 是「是(idea_root)」,由「表示(VE)」透過 ccomp 的箭頭指到。

在特定情況會需要對 match 的結果進行後處理,本次範例如下:

matches_sorted = sorted(matches, key=lambda x: abs(x[1][0] - x[1][1]))
    if len(matches_sorted) > 1:
      matches_sorted = [match for match in matches_sorted if (match[1][0] == matches_sorted[0][1][0] and match[1][1] == matches_sorted[0][1][1])]
    print(matches_sorted)

此段程式碼考量:

  • 在找的到兩個以上 who_rootVE 配對的情況下,只選則who_rootVE 距離最近的配對結果。

將結果存成 span 並視覺化

由於可以看到像是「唐(who_root)」或是「是(idea_root)」只是單一個 Token,需要配合其在 Dependency Parsing 結果中的子樹才能獲得完整資訊,所以要將 match 的結果以 span 的形式儲存在 doc

if len(matches_sorted) > 0:
      first_match = matches_sorted[0]
      VE_id = first_match[1][0]
      who_root_id = first_match[1][1]

      VE_span = Span(doc, VE_id, VE_id+1, label="VE")
      who_root_span = Span(doc, doc[who_root_id].left_edge.i, doc[who_root_id].right_edge.i+1, label="WHO")

      idea_spans = []
      for match in matches_sorted:
        match_id, token_ids = match
        idea_root_id = token_ids[2]
        idea_spans.append(Span(doc, doc[idea_root_id].left_edge.i, doc[idea_root_id].right_edge.i+1, label="IDEA"))

      doc.spans["sc"] = spacy.util.filter_spans([VE_span, who_root_span] + idea_spans)
      for span in doc.spans["sc"]:
        print(span.text, span.label_)

會得到以下輸出:

唐鳳 WHO
表示 VE
監理業務不屬於數位部範圍,面對大型跨境數位平台,最重要的是要確保現實世界中覺得合理的價值,平台上也應該符合相關的社會價值,遵守常規 IDEA

上面程式碼中的 spacy.util.filter_spans() 是為了過濾掉重複的 span。

最後使用下面程式碼即可將標註結果視覺化:

displacy.render(doc, style="span", jupyter=True)

示意圖如下:
https://ithelp.ithome.com.tw/upload/images/20220928/20152690eGtXJg0Pog.png


整篇範例新聞的視覺化結果

以下為本次所設計的 pattern 所抓到具有標注的段落視覺化結果:
https://ithelp.ithome.com.tw/upload/images/20220928/20152690GHTHGMn5CX.png

目前的 pattern 規則仍存在許多缺陷,還需要後續改進。


寫的有些匆忙,如果文章有錯誤,歡迎指正~
/images/emoticon/emoticon41.gif


上一篇
[Day-12] 解析 Spacy 的 Dependency Parsing
下一篇
[Day-14] 開始使用 Label Studio
系列文
基於自然語言處理的新聞意見提取應用開發筆記17
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言